/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.XPath;
using DaveSexton.DocProject.Engine;

namespace DaveSexton.DocProject.Sandcastle
{
	public sealed class MergeReflectionFilesBuildStep : BuildStep<SandcastleBuildEngine>
	{
		#region Public Properties
		public override string Name
		{
			get
			{
				return "Merge Reflection Files";
			}
		}
		#endregion

		#region Private / Protected
		private static readonly XPathExpression assemblyExpression = XPathExpression.Compile("//assemblies/assembly");
		private static readonly XPathExpression namespaceApiExpression = XPathExpression.Compile("//apis/api[starts-with(@id,'N:')]");
		private static readonly XPathExpression namespaceElementsExpression = XPathExpression.Compile("elements/element");
		private static readonly XPathExpression nonNamespaceApiExpression = XPathExpression.Compile("//apis/api[not(starts-with(@id,'N:'))]");
		private readonly string masterFileName;
		private readonly bool mergeIntoMaster = true;
		private readonly IList<string> reflectionFiles;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="MergeReflectionFilesBuildStep" /> class.
		/// </summary>
		public MergeReflectionFilesBuildStep(SandcastleBuildEngine engine, string masterFileName, IList<string> reflectionFiles)
			: base(engine)
		{
			this.masterFileName = masterFileName;
			this.reflectionFiles = reflectionFiles;
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="MergeReflectionFilesBuildStep" /> class.
		/// </summary>
		public MergeReflectionFilesBuildStep(SandcastleBuildEngine engine, string masterFileName, IList<string> reflectionFiles, bool mergeIntoMaster)
			: this(engine, masterFileName, reflectionFiles)
		{
			this.mergeIntoMaster = mergeIntoMaster;
		}
		#endregion

		#region Methods
		protected override void ExecuteInternal(BuildContext context)
		{
			context.TraceLine();

			string master = Path.Combine(context.CurrentDirectory, masterFileName);

			XmlDocument document = new XmlDocument();
			document.PreserveWhitespace = true;
			
			bool documentLoaded;

			if (mergeIntoMaster)
				documentLoaded = LoadDocument(document, master);
			else if (reflectionFiles.Count > 1)
			{
				string file = reflectionFiles[0];
				reflectionFiles.RemoveAt(0);

				documentLoaded = LoadDocument(document, file);
			}
			else
			{
				context.AddWarningToList("Two or more input files are required for a merge.", true);
				return;
			}

			context.TraceLine("Merging reflection data...");

			foreach (string file in reflectionFiles)
			{
				if (!documentLoaded)
				{
					if (LoadDocument(document, file))
					{
						documentLoaded = true;
						continue;
					}
				}

				if (context.Canceled)
					return;

				MergeXml(document, file);
			}

			if (context.Canceled)
				return;

			if (!documentLoaded)
				throw new InvalidOperationException(Resources.Errors.ReflectionFileNotFound);

			document.Save(master);

			context.TraceLine("Done.");
		}

		[System.Diagnostics.DebuggerHidden]
		private static bool LoadDocument(XmlDocument document, string file)
		{
			try
			{
				document.Load(file);
				return true;
			}
			catch (DirectoryNotFoundException) { }
			catch (FileNotFoundException) { }

			return false;
		}

		private static void MergeXml(XmlDocument document, string file)
		{
			XPathDocument sourceDocument = new XPathDocument(file);
			XPathNavigator source = sourceDocument.CreateNavigator();
			XPathNavigator target = document.CreateNavigator();

			ImportAssemblies(source, target);
			ImportNamespaces(source, target);
			ImportApis(source, target);
		}

		private static void ImportAssemblies(XPathNavigator source, XPathNavigator target)
		{
			XPathNavigator targetAssembliesRoot = target.SelectSingleNode("//assemblies");

			List<XPathNavigator> sourceAssemblies = new List<XPathNavigator>(16);
			List<XPathNavigator> targetAssemblies = new List<XPathNavigator>(32);

			foreach (XPathNavigator navigator in source.Select(assemblyExpression))
				sourceAssemblies.Add(navigator);

			foreach (XPathNavigator navigator in target.Select(assemblyExpression))
				targetAssemblies.Add(navigator);

			foreach (XPathNavigator sourceAssembly in sourceAssemblies)
			{
				bool matchFound = false;

				foreach (XPathNavigator targetAssembly in targetAssemblies)
				{
					if (string.Equals(sourceAssembly.GetAttribute("name", ""), targetAssembly.GetAttribute("name", ""), StringComparison.Ordinal))
					{
						XPathNavigator sourceData = sourceAssembly.SelectSingleNode("assemblydata");
						XPathNavigator targetData = targetAssembly.SelectSingleNode("assemblydata");

						if ((sourceData == null && targetData == null) || (sourceData != null && targetData != null
							&& string.Equals(sourceData.GetAttribute("version", ""), targetData.GetAttribute("version", ""), StringComparison.OrdinalIgnoreCase)
							&& string.Equals(sourceData.GetAttribute("culture", ""), targetData.GetAttribute("culture", ""), StringComparison.OrdinalIgnoreCase)
							&& string.Equals(sourceData.GetAttribute("key", ""), targetData.GetAttribute("key", ""), StringComparison.Ordinal)
							&& string.Equals(sourceData.GetAttribute("hash", ""), targetData.GetAttribute("hash", ""), StringComparison.OrdinalIgnoreCase)))
						{
							matchFound = true;
							break;
						}
					}
				}

				if (!matchFound)
					targetAssembliesRoot.AppendChild(sourceAssembly);
			}
		}

		private static void ImportNamespaces(XPathNavigator source, XPathNavigator target)
		{
			XPathNavigator targetApisRoot = target.SelectSingleNode("//apis");

			List<XPathNavigator> sourceNamespaces = new List<XPathNavigator>(64);
			List<XPathNavigator> targetNamespaces = new List<XPathNavigator>(128);

			foreach (XPathNavigator navigator in source.Select(namespaceApiExpression))
				sourceNamespaces.Add(navigator);

			foreach (XPathNavigator navigator in target.Select(namespaceApiExpression))
				targetNamespaces.Add(navigator);

			foreach (XPathNavigator sourceNamespace in sourceNamespaces)
			{
				bool matchFound = false;

				foreach (XPathNavigator targetNamespace in targetNamespaces)
				{
					if (string.Equals(sourceNamespace.GetAttribute("id", ""), targetNamespace.GetAttribute("id", ""), StringComparison.Ordinal))
					{
						matchFound = true;

						ImportNamespaceElements(sourceNamespace, targetNamespace);
						break;
					}
				}

				if (!matchFound)
					targetApisRoot.AppendChild(sourceNamespace);
			}
		}

		private static void ImportNamespaceElements(XPathNavigator source, XPathNavigator target)
		{
			XPathNavigator targetElementsRoot = target.SelectSingleNode("elements");

			// NOTE: In testing MRefBuilder.exe outputs duplicate <element> tags for types with the same name and namespace in different assemblies; 
			// therefore, duplicate elements are not prevented during the merge operation here either.

			foreach (XPathNavigator sourceElement in source.Select(namespaceElementsExpression))
				targetElementsRoot.AppendChild(sourceElement);
		}

		private static void ImportApis(XPathNavigator source, XPathNavigator target)
		{
			XPathNavigator targetApisRoot = target.SelectSingleNode("//apis");

			// NOTE: Valid API elements should have a <containers> element that indicates the assembly in which the API is defined.
			// Presumeably, this is used by Sandcastle for when a type with the same name and namespace is defined in multiple assemblies.
			// The merge operation below has no need to verify whether duplicate API elements are being added since they must have a 
			// <containers> element in order to be valid and the user is responsible for not adding duplicate reflection files as external sources;
			// (i.e., this method does not check the validity of non-namespace API elements in the "source" reflection file).  In the event 
			// of identical duplicate APIs in the master reflection file, the effect that will have on Sandcastle is unknown.

			foreach (XPathNavigator sourceApi in source.Select(nonNamespaceApiExpression))
				targetApisRoot.AppendChild(sourceApi);
		}

		protected override void CancelInternal(BuildContext context)
		{
			// do nothing
		}
		#endregion
	}
}
